Skip to content

Add printer eyfj07 ngiot#1532

Closed
Astute4185 wants to merge 44 commits into
DeebotUniverse:devfrom
Astute4185:add-printer-eyfj07-ngiot
Closed

Add printer eyfj07 ngiot#1532
Astute4185 wants to merge 44 commits into
DeebotUniverse:devfrom
Astute4185:add-printer-eyfj07-ngiot

Conversation

@Astute4185

Copy link
Copy Markdown

Summary

This PR introduces end-to-end NGIOT support for newer Ecovacs devices, with initial hardware coverage for eyfj07. It adds the NGIOT transport and authentication stack, wires NGIOT command execution into the existing client architecture, expands MQTT handling for NGIOT numeric-topic live events, and adds a new NGIOT map parsing/rendering path backed by Rust helpers.

The net result is that NGIOT-backed devices can now authenticate, send control commands, receive live status and map updates, and surface those capabilities through the existing capability model without overloading the legacy command path.

Key changes

NGIOT transport and authentication

  • Added SST token support for NGIOT endpoint-control devices via sst_authentication.py
  • Added ngiot_client.py to issue NGIOT control requests with host fallback handling
  • Extended Authenticator to:
    • store NGIOT configuration
    • derive NGIOT base URLs from explicit config or device service.mqs
    • attach and tear down NGIOT helpers
    • automatically bootstrap NGIOT transport for matching hardware profiles

NGIOT command framework

  • Added deebot_client/commands/ngiot/*
  • Implemented NGIOT-backed commands for:
    • battery
    • charge / return-to-dock
    • clean, pause, resume, stop-via-dock
    • child lock
    • fan speed
    • consumable life span + reset
    • network info
    • play sound / locate
    • stats, report stats, total stats
    • volume
    • map and position
    • custom command payloads

Hardware profile support

  • Added deebot_client/hardware/eyfj07.py
  • Registered eyfj07 as a supported hardware class using NGIOT-backed capabilities for:
    • control
    • state
    • stats
    • settings
    • consumables
    • map
    • network info

MQTT live event support

  • Extended MQTT topic subscription logic to support observed NGIOT ATR topic shapes using user_id
  • Added numeric-topic message handlers for live NGIOT events:
    • 10000 for status-style payloads
    • 30000 for map-style payloads
  • Registered NGIOT message handlers in messages/json/__init__.py

NGIOT map pipeline

  • Added ngiot_map_parser.py to normalize:
    • base map metadata
    • robot pose
    • trace payloads
    • room/area geometry
    • overlays such as walls and carpets
  • Added ngiot_map_state.py to aggregate per-map state from partial NGIOT responses
  • Updated device.py to attach a shared NGIOT map state store to the event bus
  • Extended the Rust map stack to support:
    • NGIOT raster background handling
    • LZ4 map decompression
    • additional overlay styling and rendering hooks

Supporting refactors

  • Updated generic message handling to degrade more safely to ANALYSE for unsupported payload shapes instead of cascading ambiguous superclass behavior
  • Tightened event bus unsubscribe handling
  • Updated Python type stubs for expanded Rust map bindings

Functional impact

This PR enables NGIOT-backed devices to operate through the existing client abstractions while keeping the legacy path intact for older devices. For eyfj07, that includes:

  • device control through NGIOT endpoint-control requests
  • child lock, fan speed, volume, locate/play-sound, and consumable reset support
  • stats and lifetime totals normalization
  • live MQTT status updates
  • NGIOT map ingestion, trace handling, and render-state aggregation

Testing

Added and updated tests for:

  • NGIOT clean/state mapping
  • NGIOT stats conversion and total handling
  • NGIOT map bootstrap behavior
  • NGIOT MQTT topic routing
  • NGIOT numeric-topic message dispatch
  • NGIOT client request construction and fallback behavior
  • NGIOT map parser and map state normalization
  • Rust LZ4 decompression helpers
  • hardware capability registration for eyfj07

Known limitations

  • Live NGIOT minor-map delta payloads are detected but not yet generically applied to the renderer
  • CleanAction.STOP currently maps to return-to-dock behavior rather than a confirmed stop-in-place command
  • Some NGIOT map alignment/render calibration remains based on observed device behavior and will likely need additional real-device validation across models

Reviewer focus

Please focus review on:

  • NGIOT transport bootstrap and SST lifecycle handling
  • MQTT topic matching and live event dispatch
  • map parsing/render integration and normalization assumptions
  • any capability contract regressions introduced by the new eyfj07 profile

Astute4185 and others added 30 commits March 27, 2026 08:23
- add NGIOT endpoint-control request path for eco-ng devices
- wire eyfj07 status reads through APN 10001
- implement captured control actions for clean, pause, resume, return, cancel return, locate, and area clean
- add fan mode get/set support using captured NGIOT fanMode payloads
- add volume set support using captured NGIOT volume payloads
- correct SST authentication module naming/import issues
- align command handling with dedicated 400xx/500xx APNs instead of incorrect 10001 writes
- keep mop/map support out of scope for now (Lost the mop attachment).
Basic functionality is now working in HA - but still trying to get mapping, tracing and positioning of map working.
Added tests for decompressing LZ4 base64 data and handling errors.
Refactor _handle_body_data_dict to process mapInfos and determine active and fallback map IDs.
Change MapData.update_positions so partial updates merge by position type instead of replacing the whole list.
This module provides functionality to parse and normalize NGIOT mapping payloads into structured Python data types, including points, poses, maps, areas, traces, and overlays.
This module aggregates NGIOT map state, managing snapshots and providing normalized views for rendering.
…m subsets, and refactor related event handling
Astute4185 and others added 14 commits April 7, 2026 19:19
Enhance NGIOT map handling by adding direction support and improving map ID resolution logic
…cleanTime to seconds; modify GetTotalStats to expose total stats and convert cleanTimeTotal to seconds. Increase MAX_VOLUME to 10 in SetVolume command.
… encapsulation; add tests for ngiot_map_parser functionality
…lities

- Implement tests for clean commands including state mapping and request generation.
- Add tests for statistics retrieval and validation.
- Introduce tests for NGIOT map state management, ensuring proper snapshot normalization.
- Enhance MQTT client tests to cover NGIOT topic handling and message routing.
- Include utility tests for decompression functions with real payloads.
@Astute4185

Copy link
Copy Markdown
Author

It looks like GitHub created the CI and CodeQL workflow runs for this PR, but both completed with no jobs. Could someone with write access please check whether this PR is waiting on fork-workflow approval and, if so, approve the workflows to run or rerun CI from Actions? If needed, I can push a no-op commit to retrigger the pull_request workflows.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds end-to-end NGIOT support (transport/auth, commands, MQTT live events, and map rendering) with initial hardware coverage for the eyfj07 device class, integrating NGIOT-backed devices into the existing capability and event/message architecture.

Changes:

  • Introduces NGIOT transport + SST authentication and wires it into Authenticator/command execution.
  • Adds NGIOT MQTT numeric-topic handling (e.g., 10000 status, 30000 map) and NGIOT map parsing/state aggregation.
  • Extends the Rust map pipeline for NGIOT raster backgrounds + LZ4 trace/map decompression and new overlay styling.

Reviewed changes

Copilot reviewed 55 out of 55 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/test_ngiot_map_state.py Adds unit tests for NGIOT map snapshot aggregation and normalization.
tests/test_ngiot_map_parser.py Adds tests for NGIOT base-map parsing behavior and lz4Len capture.
tests/test_ngiot_client.py Adds tests for NGIOT client request payload defaults, host fallback, and response validation.
tests/test_mqtt_client_ngiot.py Adds tests for NGIOT topic subscription shapes and numeric-topic ATR routing.
tests/test_message.py Updates expectations for safer “analyse” fallback behavior in message handling.
tests/test_map.py Adjusts map tests for updated callback signature and formatting fixes.
tests/rs/test_util.py Adds tests for new Rust LZ4 decompression helper (including a real payload hash).
tests/rs/test_map.py Updates Rust map error message expectations to include NGIOT lz4_len context.
tests/messages/json/test_ngiot.py Adds tests for NGIOT numeric-topic message resolution and dispatch.
tests/helpers/init.py Updates test capability mocks to include map attribute.
tests/hardware/test_init.py Registers eyfj07 hardware profile and validates capability event extraction.
tests/conftest.py Extends EventBus fixtures with an NGIOT map state store for tests.
tests/commands/ngiot/test_stats.py Adds tests for NGIOT stats conversion/normalization.
tests/commands/ngiot/test_map.py Adds tests for NGIOT cached map info handling and map set bootstrap.
tests/commands/ngiot/test_clean.py Adds tests for NGIOT clean command request mapping and state mapping helpers.
src/util.rs Adds Rust LZ4 base64 decompression helper and exposes it to Python bindings.
src/map/style.rs Extends SVG styling classes for NGIOT overlays (room subset, carpet area).
src/map/points.rs Adds LZ4 trace extraction path, scale controls, and NGIOT-aware trace transforms.
src/map/ngiot_background.rs Adds NGIOT raster background decoding/rendering logic (PNG generation + viewbox).
src/map/mod.rs Integrates NGIOT background rendering, new subset styling, and viewbox changes.
src/map/map_info.rs Refactors map-info parsing via a shared method and updates viewbox float handling.
src/map/background_image.rs Adjusts background image cropping to updated float-based viewbox types.
deebot_client/sst_authentication.py Adds SST token minting/caching/refresh logic for NGIOT endpoint-control devices.
deebot_client/rs/map.pyi Updates Python type stubs for new NGIOT background + trace/map helpers and signatures.
deebot_client/ngiot_map_state.py Adds NGIOT map snapshot state store with normalization support.
deebot_client/ngiot_map_parser.py Adds NGIOT map payload parsing and coordinate normalization helpers.
deebot_client/ngiot_client.py Implements NGIOT endpoint-control client with SST auth, host fallback, and payload defaults.
deebot_client/mqtt_client.py Extends MQTT subscription/topic matching to include NGIOT user-id topic shapes.
deebot_client/messages/json/ngiot.py Adds NGIOT numeric-topic MQTT message handlers for status and map live events.
deebot_client/messages/json/init.py Registers NGIOT JSON message handlers in the message registry.
deebot_client/message.py Refactors message dispatch to safely fall back to ANALYSE for unsupported payload types/shapes.
deebot_client/map.py Integrates NGIOT map state/background sync, LZ4 trace handling, and subset keying changes.
deebot_client/hardware/eyfj07.py Adds eyfj07 hardware profile wired to NGIOT-backed capabilities/commands.
deebot_client/events/map.py Extends MapSetType (adds carpets) and adds NGIOT lz4_len to MapTraceEvent.
deebot_client/event_bus.py Hardens unsubscribe logic for on-subscription callbacks.
deebot_client/device.py Attaches a shared NGIOT map state store to the device event bus.
deebot_client/commands/ngiot/volume.py Adds NGIOT volume get/set command support.
deebot_client/commands/ngiot/stats.py Adds NGIOT stats/report/total command handling with time unit normalization.
deebot_client/commands/ngiot/pos.py Provides NGIOT position command compatibility export.
deebot_client/commands/ngiot/play_sound.py Adds NGIOT play-sound/locate command.
deebot_client/commands/ngiot/network.py Adds NGIOT network info parsing/dispatch.
deebot_client/commands/ngiot/map.py Adds NGIOT map commands (cached info, major map, trace, subsets/overlays).
deebot_client/commands/ngiot/locate.py Adds NGIOT locate-device command (seek) compatibility.
deebot_client/commands/ngiot/life_span.py Adds NGIOT consumable life-span get/reset support.
deebot_client/commands/ngiot/fan_speed.py Adds NGIOT fan speed get/set command support.
deebot_client/commands/ngiot/error.py Adds NGIOT error extraction and event dispatch.
deebot_client/commands/ngiot/custom.py Adds NGIOT “custom payload” command support.
deebot_client/commands/ngiot/common.py Adds NGIOT command mixins for bootstrapping NGIOT client via Authenticator.
deebot_client/commands/ngiot/clean.py Adds NGIOT clean/area-clean commands and NGIOT state mapping helpers.
deebot_client/commands/ngiot/child_lock.py Adds NGIOT child-lock get/set support.
deebot_client/commands/ngiot/charge.py Adds NGIOT return-to-dock command support.
deebot_client/commands/ngiot/battery.py Adds NGIOT battery/availability handling.
deebot_client/commands/ngiot/init.py Exposes NGIOT commands and command registry.
deebot_client/authentication.py Adds NGIOT configuration + bootstrap support (deriving NGIOT base URL, attaching helpers).
Cargo.toml Adds Rust dependency for LZ4 block decompression (lz4_flex).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +321 to +325
def _resolve_atr_subscription(self, topic_split: list[str]) -> SubscriberInfo | None:
for info in self._subscriptions.values():
if self._topic_matches_device(topic_split, info.device_info):
return info
return None
Comment on lines +600 to +603
event_bus.notify(MapSetEvent(self._map_type, subset_ids, map_id))
return HandlingResult.success()


Comment thread src/util.rs
Comment on lines +49 to +53
if written < expected_len {
output.truncate(written);
return Ok(output);
}

Comment thread src/util.rs
Comment on lines +94 to +99
fn python_decompress_base64_lz4_data(value: &str, expected_len: usize) -> Result<Vec<u8>, PyErr> {
decompress_base64_lz4_data(value, expected_len).map_err(|err| {
error!(
"Error decompressing LZ4 base64 data: {err}; expected_len:{expected_len}; value:{value}"
);
PyValueError::new_err(err.to_string())
Comment thread src/map/points.rs
Comment on lines +190 to +195
.map_err(|err| {
error!(
"Failed to extract trace points: {err};value:{value};lz4_len:{:?}",
lz4_len
);
PyValueError::new_err(err.to_string())

@edenhaus edenhaus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR :)

I did an initial review but the PR contains some unrelated changes like rearranging code unnecessary or even docs string updates, which can be done in a separate PR to keep this one as small as possible.

Please make sure CI is passing and that you have added tests for all new code.
As ngiot is using also json, can we maybe extend the json one as it looks like some commands are similar to the one in json. If not can we split this PR into smaller pieces so reviewing is easier. Like one PR for the map, one per command and so.

Comment thread deebot_client/message.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change this file?

# iot/atr/[channel]/[user-id]/[device-class]/[device-id]/[data_type]
# The final device identifier can vary between observed payloads, so
# subscribe to both known device identifiers.
for device_id in dict.fromkeys((api['did'], api['resource'])):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are both needed?

# The final device identifier can vary between observed payloads, so
# subscribe to both known device identifiers.
for device_id in dict.fromkeys((api['did'], api['resource'])):
topics.append(f"iot/atr/+/{user_id}/{api['class']}/{device_id}/{data_type}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to specify the user_id or can we use + to allow all?

def unsubscribe() -> None:
data.unsubscribe()
event_processing_data.on_subscription_callbacks.remove(data)
if data in event_processing_data.on_subscription_callbacks:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is the callback not present?

Comment thread deebot_client/device.py
Comment on lines +74 to +77
self.ngiot_map_state = NgiotMapStateStore()
setattr(self.events, "_ngiot_map_state_store", self.ngiot_map_state)
# Optional public alias for debugging/introspection.
self.events.ngiot_map_state = self.ngiot_map_state

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's hacky.... Don't set a protected fields from outside



@unique
class MapSetType(StrEnum):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert any changes where you just move things around as this PR is big and I want to keep the changes as small as possible

@Astute4185

Copy link
Copy Markdown
Author

Thanks for the feedback @edenhaus — I agree this will be easier to review and maintain as a split-up sequence.

I am going to close this PR and rework it into a smaller PR sequence.
I will keep this branch as a working reference and reopen the work as smaller, reviewable PR.

@Astute4185 Astute4185 closed this Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants